import "@site/src/languages/highlight";
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# 使用动画节点

&emsp;&emsp;在游戏开发中，动画是使角色和场景生动起来的重要元素。Dora SSR 引擎提供了一个强大的动画处理节点类——**Playable**。它是三种动画系统的基础类：

- **Model**：
	- Dora SSR 引擎实现的骨骼动画系统。
	- 动画模型通常由一个 `.model` 文件，一个 `.clip` 文件和一个 `.png` 文件组成。
- **DragonBone**：
	- 开源的 DragonBones 动画系统。
	- 动画模型通常由一个以 `_ske.json` 结尾的文件，一个以 `_tex.json` 结尾的文件和一个以 `_tex.png` 结尾的图片文件组成。
- **Spine**：
	- 著名的商业动画软件 Spine2D 的动画系统。
	- 动画模型通常由一个 `.json` (或是 `.skel`) 文件，一个 `.atlas` 文件和一个 `.png` 文件组成。

&emsp;&emsp;本教程将指导您如何在程序中使用 Playable 节点，涵盖从加载动画到控制播放的各个方面。

## 1. 创建 Playable 实例

&emsp;&emsp;要使用 Playable 节点，首先需要创建其实例。Playable 支持三种动画系统，通常使用的加载方式如下：

- **Model 文件**：`"model:"` 前缀 + 不带后缀的模型文件路径。
- **Spine 文件**：`"spine:"` 前缀 + 不带后缀的 Spine 文件路径。
- **DragonBones 文件**：`"bone:"` 前缀 + 不带后缀的 DragonBones 文件路径。

### 1.1 示例：加载 Model 动画

<Tabs groupId="language-select">
<TabItem value="lua" label="Lua">

```lua
local Playable <const> = require("Playable")

-- 加载 Model 动画
local modelPath = "model:assets/character"
local character = Playable(modelPath)

if character then
	character.position = Vec2(100, 200)
	stage:addChild(character)
else
	print("加载 Model 动画失败！")
end
```

</TabItem>
<TabItem value="tl" label="Teal">

```tl
local Playable <const> = require("Playable")

-- 加载 Model 动画
local modelPath = "model:assets/character"
local character = Playable(modelPath)

if not character is nil then
	character.position = Vec2(100, 200)
	stage:addChild(character)
else
	print("加载 Model 动画失败！")
end
```

</TabItem>
<TabItem value="ts" label="TypeScript">

```ts
import { Playable, Vec2 } from "Dora";

// 加载 Model 动画
const modelPath = "model:assets/character";
const character = Playable(modelPath);

if (character) {
	character.position = Vec2(100, 200);
	stage.addChild(character);
} else {
	print("加载 Model 动画失败！");
}
```

</TabItem>
<TabItem value="yue" label="YueScript">

```yue
_ENV = Dora

// 加载 Model 动画
modelPath = "model:assets/character"
character = Playable modelPath

if character
	with character
		.position = Vec2 100, 200
		\addTo stage
else
	print "加载 Model 动画失败！"
```

</TabItem>
</Tabs>

### 1.2 示例：加载 Spine 动画

<Tabs groupId="language-select">
<TabItem value="lua" label="Lua">

```lua
local Playable <const> = require("Playable")

-- 加载 Spine 动画
local spinePath = "spine:assets/monster"
local monster = Playable(spinePath)

if monster then
	monster.position = Vec2(300, 200)
	stage:addChild(monster)
else
	print("加载 Spine 动画失败！")
end
```

</TabItem>
<TabItem value="tl" label="Teal">

```tl
local Playable <const> = require("Playable")

-- 加载 Spine 动画
local spinePath = "spine:assets/monster"
local monster = Playable(spinePath)

if not monster is nil then
	monster.position = Vec2(300, 200)
	stage:addChild(monster)
else
	print("加载 Spine 动画失败！")
end
```

</TabItem>
<TabItem value="ts" label="TypeScript">

```ts
import { Playable, Vec2 } from "Dora";

// 加载 Spine 动画
const spinePath = "spine:assets/monster";
const monster = Playable(spinePath);

if (monster) {
	monster.position = Vec2(300, 200);
	stage.addChild(monster);
} else {
	print("加载 Spine 动画失败！");
}
```

</TabItem>
<TabItem value="yue" label="YueScript">

```yue
_ENV = Dora

// 加载 Spine 动画
spinePath = "spine:assets/monster"
monster = Playable spinePath

if monster
	with monster
		.position = Vec2 300, 200
		\addTo stage
else
	print "加载 Spine 动画失败！"
```

</TabItem>
</Tabs>

### 1.3 示例：加载 DragonBones 动画

<Tabs groupId="language-select">
<TabItem value="lua" label="Lua">

```lua
local Playable <const> = require("Playable")

-- 加载 DragonBones 动画
local dragonBonePath = "bone:assets/dragon"
local dragon = Playable(dragonBonePath)

if dragon then
	dragon.position = Vec2(500, 200)
	stage:addChild(dragon)
else
	print("加载 DragonBones 动画失败！")
end
```

</TabItem>
<TabItem value="tl" label="Teal">

```tl
local Playable <const> = require("Playable")

-- 加载 DragonBones 动画
local dragonBonePath = "bone:assets/dragon"
local dragon = Playable(dragonBonePath)

if not dragon is nil then
	dragon.position = Vec2(500, 200)
	stage:addChild(dragon)
else
	print("加载 DragonBones 动画失败！")
end
```

</TabItem>
<TabItem value="ts" label="TypeScript">

```ts
import { Playable, Vec2 } from "Dora";

// 加载 DragonBones 动画
const dragonBonePath = "bone:assets/dragon";
const dragon = Playable(dragonBonePath);

if (dragon) {
	dragon.position = Vec2(500, 200);
	stage.addChild(dragon);
} else {
	print("加载 DragonBones 动画失败！");
}
```

</TabItem>
<TabItem value="yue" label="YueScript">

```yue
_ENV = Dora

// 加载 DragonBones 动画
dragonBonePath = "bone:assets/dragon"
dragon = Playable dragonBonePath

if dragon
	with dragon
		.position = Vec2 500, 200
		\addTo stage
else
	print "加载 DragonBones 动画失败！"
```

</TabItem>
</Tabs>

### 1.4 示例：异步加载动画

&emsp;&emsp;在实际开发中，加载动画可能需要一些时间。您可以使用 `Cache:loadAsync()` 方法异步加载动画，加载完成后执行回调函数。

<Tabs groupId="language-select">
<TabItem value="lua" label="Lua">

```lua
local Playable <const> = require("Playable")
local thread <const> = require("thread")

-- 异步加载 Model 动画
local modelPath = "model:assets/character"
thread(function()
	if Cache:loadAsync(modelPath) then
		local character = Playable(modelPath)
		character.position = Vec2(100, 200)
		stage:addChild(character)
	else
		print("异步加载 Model 动画失败！")
	end
end)
```

</TabItem>
<TabItem value="tl" label="Teal">

```tl
local Playable <const> = require("Playable")
local thread <const> = require("thread")

-- 异步加载 Model 动画
local modelPath = "model:assets/character"
thread(function()
	if Cache:loadAsync(modelPath) then
		local character = Playable(modelPath)
		if not character is nil then
			character.position = Vec2(100, 200)
			stage:addChild(character)
		end
	else
		print("异步加载 Model 动画失败！")
	end
end)
```

</TabItem>
<TabItem value="ts" label="TypeScript">

```ts
import { Playable, Vec2, Cache, thread } from "Dora";

// 异步加载 Model 动画
const modelPath = "model:assets/character";
thread(() => {
	if (Cache.loadAsync(modelPath)) {
		const character = Playable(modelPath);
		if (character) {
			character.position = Vec2(100, 200);
			stage.addChild(character);
		}
	} else {
		print("异步加载 Model 动画失败！");
	}
});
```

</TabItem>
<TabItem value="yue" label="YueScript">

```yue
_ENV = Dora

-- 异步加载 Model 动画
modelPath = "model:assets/character"
thread ->
	if Cache\loadAsync modelPath
		with Playable modelPath
			.position = Vec2 100, 200
			\addTo stage
	else
		print "异步加载 Model 动画失败！"
```

</TabItem>
</Tabs>

## 2. 播放动画

&emsp;&emsp;创建实例后，可以使用 `play` 方法播放指定的动画。

<Tabs groupId="language-select">
<TabItem value="lua" label="Lua">

```lua
-- 播放动画 "run"，并循环播放
local duration = character:play("run", true)
```

</TabItem>
<TabItem value="tl" label="Teal">

```tl
-- 播放动画 "run"，并循环播放
local duration = character:play("run", true)
```

</TabItem>
<TabItem value="ts" label="TypeScript">

```ts
// 播放动画 "run"，并循环播放
const duration = character.play("run", true);
```

</TabItem>
<TabItem value="yue" label="YueScript">

```yue
// 播放动画 "run"，并循环播放
duration = character\play "run", true
```

</TabItem>
</Tabs>

- **参数说明**：
	- `name`：要播放的动画名称。
	- `loop`（可选）：是否循环播放，默认为 `false`。
- **返回值**：动画的持续时间（秒）。

## 3. 停止动画

&emsp;&emsp;使用 `stop` 方法可以停止当前正在播放的动画。

<Tabs groupId="language-select">
<TabItem value="lua" label="Lua">

```lua
-- 停止动画
character:stop()
```

</TabItem>
<TabItem value="tl" label="Teal">

```tl
-- 停止动画
character:stop()
```

</TabItem>
<TabItem value="ts" label="TypeScript">

```ts
// 停止动画
character.stop();
```

</TabItem>
<TabItem value="yue" label="YueScript">

```yue
// 停止动画
character\stop()
```

</TabItem>
</Tabs>

## 4. 设置播放速度

&emsp;&emsp;通过调整 `speed` 属性，可以改变动画的播放速度。

<Tabs groupId="language-select">
<TabItem value="lua" label="Lua">

```lua
-- 将播放速度加倍
character.speed = 2.0
```

</TabItem>
<TabItem value="tl" label="Teal">

```tl
-- 将播放速度加倍
character.speed = 2.0
```

</TabItem>
<TabItem value="ts" label="TypeScript">

```ts
// 将播放速度加倍
character.speed = 2.0;
```

</TabItem>
<TabItem value="yue" label="YueScript">

```yue
// 将播放速度加倍
character.speed = 2.0
```

</TabItem>
</Tabs>

- **注意**：`speed` 的默认值为 `1.0`。

## 5. 翻转动画

&emsp;&emsp;使用 `fliped` 属性可以水平翻转动画，常用于角色转向。

<Tabs groupId="language-select">
<TabItem value="lua" label="Lua">

```lua
-- 水平翻转
character.fliped = true
```

</TabItem>
<TabItem value="tl" label="Teal">

```tl
-- 水平翻转
character.fliped = true
```

</TabItem>
<TabItem value="ts" label="TypeScript">

```ts
// 水平翻转
character.fliped = true;
```

</TabItem>
<TabItem value="yue" label="YueScript">

```yue
// 水平翻转
character.fliped = true
```

</TabItem>
</Tabs>

- **`fliped`**：`true` 表示翻转，`false` 表示正常。

## 6. 获取关键点坐标

&emsp;&emsp;`getKey` 方法用于获取模型上关键点的坐标，例如角色的手. 脚位置。在 Model 动画系统中，关键点是模型上设置的特定点。在 DragonBone 中，关键点是骨骼的位置。在 Spine2D 中，关键点是顶点附件的位置。

<Tabs groupId="language-select">
<TabItem value="lua" label="Lua">

```lua
-- 获取角色右手的坐标
local handPosition = character:getKey("right_hand")
print("右手坐标：", handPosition.x, handPosition.y)
```

</TabItem>
<TabItem value="tl" label="Teal">

```tl
-- 获取角色右手的坐标
local handPosition = character:getKey("right_hand")
print("右手坐标：", handPosition.x, handPosition.y)
```

</TabItem>
<TabItem value="ts" label="TypeScript">

```ts
// 获取角色右手的坐标
const handPosition = character.getKey("right_hand");
print("右手坐标：", handPosition.x, handPosition.y);
```

</TabItem>
<TabItem value="yue" label="YueScript">

```yue
-- 获取角色右手的坐标
handPosition = character\getKey "right_hand"
print "右手坐标：", handPosition.x, handPosition.y
```

</TabItem>
</Tabs>

- **参数**：关键点名称（字符串）。
- **返回值**：`Vec2`，表示关键点的坐标。

## 7. 使用插槽添加子节点

&emsp;&emsp;`setSlot` 方法允许在模型的特定插槽上添加子节点，例如为角色添加武器或装备。

<Tabs groupId="language-select">
<TabItem value="lua" label="Lua">

```lua
-- 创建武器精灵
local sword = Sprite("assets/sword.png")

-- 将武器添加到角色的 "right_hand" 插槽
character:setSlot("right_hand", sword)
```

</TabItem>
<TabItem value="tl" label="Teal">

```tl
-- 创建武器精灵
local sword = Sprite("assets/sword.png")

-- 将武器添加到角色的 "right_hand" 插槽
character:setSlot("right_hand", sword)
```

</TabItem>
<TabItem value="ts" label="TypeScript">

```ts
// 创建武器精灵
const sword = Sprite("assets/sword.png");

// 将武器添加到角色的 "right_hand" 插槽
character.setSlot("right_hand", sword);
```

</TabItem>
<TabItem value="yue" label="YueScript">

```yue
-- 创建武器精灵
sword = Sprite "assets/sword.png"

-- 将武器添加到角色的 "right_hand" 插槽
character\setSlot "right_hand", sword
```

</TabItem>
</Tabs>

- **参数说明**：
	- `name`：插槽名称。
	- `item`：要添加的节点对象。

## 8. 获取插槽上的子节点

&emsp;&emsp;使用 `getSlot` 方法可以获取指定插槽上的子节点。

<Tabs groupId="language-select">
<TabItem value="lua" label="Lua">

```lua
-- 获取 "right_hand" 插槽上的节点
local equippedItem = character:getSlot("right_hand")
if equippedItem then
	print("已装备物品：", equippedItem)
else
	print("插槽为空")
end
```

</TabItem>
<TabItem value="tl" label="Teal">

```tl
-- 获取 "right_hand" 插槽上的节点
local equippedItem = character:getSlot("right_hand")
if not equippedItem is nil then
	print("已装备物品：", equippedItem)
else
	print("插槽为空")
end
```

</TabItem>
<TabItem value="ts" label="TypeScript">

```ts
// 获取 "right_hand" 插槽上的节点
const equippedItem = character.getSlot("right_hand");
if (equippedItem) {
	print("已装备物品：", equippedItem);
} else {
	print("插槽为空");
}
```

</TabItem>
<TabItem value="yue" label="YueScript">

```yue
-- 获取 "right_hand" 插槽上的节点
equippedItem = character\getSlot "right_hand"
if equippedItem
	print "已装备物品：", equippedItem
else
	print "插槽为空"
```

</TabItem>
</Tabs>

- **返回值**：`Node` 对象或 `nil`。

## 9. 监听动画结束事件

&emsp;&emsp;可以使用 `onAnimationEnd` 方法注册一个回调函数，当动画播放结束时触发。

<Tabs groupId="language-select">
<TabItem value="lua" label="Lua">

```lua
-- 注册动画结束回调
character:onAnimationEnd(function(animationName, target)
	print("动画结束：", animationName)
	-- 可以在此处进行后续操作，例如切换动画
end)
```

</TabItem>
<TabItem value="tl" label="Teal">

```tl
-- 注册动画结束回调
character:onAnimationEnd(function(animationName: string, target: Playable.Type)
	print("动画结束：", animationName)
	-- 可以在此处进行后续操作，例如切换动画
end)
```

</TabItem>
<TabItem value="ts" label="TypeScript">

```ts
// 注册动画结束回调
character.onAnimationEnd((animationName, target) => {
	print("动画结束：", animationName);
	// 可以在此处进行后续操作，例如切换动画
});
```

</TabItem>
<TabItem value="yue" label="YueScript">

```yue
-- 注册动画结束回调
character\onAnimationEnd (animationName, target) ->
	print "动画结束：", animationName
	-- 可以在此处进行后续操作，例如切换动画
```

</TabItem>
</Tabs>

- **参数说明**：
	- `callback`：回调函数，接受 `animationName` 和 `target` 两个参数。

## 10. 综合示例

&emsp;&emsp;以下是一个综合示例，演示如何创建角色，播放动画，添加装备，并处理动画结束事件。

<Tabs groupId="language-select">
<TabItem value="lua" label="Lua">

```lua
local Playable <const> = require("Playable")
local Sprite <const> = require("Sprite")
local Vec2 <const> = require("Vec2")
local sleep <const> = require("sleep")

-- 创建角色
local character = Playable("model:assets/hero.model")
character.position = Vec2(200, 300)

-- 设置属性
character.speed = 1.0
character.fliped = false

-- 播放待机动画
character:play("idle", true)

-- 创建武器并装备
local sword = Sprite("assets/sword.png")
character:setSlot("right_hand", sword)

-- 注册动画结束事件
character:onAnimationEnd(function(animationName, target)
	if animationName == "attack" then
		-- 攻击结束后返回待机状态
		target:play("idle", true)
	end
end)

-- 每隔3秒执行一次攻击
character:loop(function()
	-- 播放攻击动画
	character:play("attack")
	sleep(3)
end)
```

</TabItem>
<TabItem value="tl" label="Teal">

```tl
local Playable <const> = require("Playable")
local Sprite <const> = require("Sprite")
local Vec2 <const> = require("Vec2")
local sleep <const> = require("sleep")

-- 创建角色
local character = Playable("model:assets/hero.model")
if not character is nil then
	character.position = Vec2(200, 300)

	-- 设置属性
	character.speed = 1.0
	character.fliped = false

	-- 播放待机动画
	character:play("idle", true)

	-- 创建武器并装备
	local sword = Sprite("assets/sword.png")
	character:setSlot("right_hand", sword)

	-- 注册动画结束事件
	character:onAnimationEnd(function(animationName: string, target: Playable.Type)
		if animationName == "attack" then
			-- 攻击结束后返回待机状态
			target:play("idle", true)
		end
	end)

	-- 每隔3秒执行一次攻击
	character:loop(function(): boolean
		-- 播放攻击动画
		character:play("attack")
		sleep(3)
		return false
	end)
end
```

</TabItem>
<TabItem value="ts" label="TypeScript">

```ts
import { Playable, Sprite, Vec2, sleep } from "Dora";

// 创建角色
const character = Playable("model:assets/hero.model");
if (character) {
	character.position = Vec2(200, 300);

	// 设置属性
	character.speed = 1.0;
	character.fliped = false;

	// 播放待机动画
	character.play("idle", true);

	// 创建武器并装备
	const sword = Sprite("assets/sword.png");
	character.setSlot("right_hand", sword);

	// 注册动画结束事件
	character.onAnimationEnd((animationName, target) => {
		if (animationName === "attack") {
			// 攻击结束后返回待机状态
			target.play("idle", true);
		}
	});

	// 每隔3秒执行一次攻击
	character.loop(() => {
		// 播放攻击动画
		character.play("attack");
		sleep(3);
		return false;
	});
}
```

</TabItem>
<TabItem value="yue" label="YueScript">

```yue
_ENV = Dora

-- 创建角色
with Playable "model:assets/hero.model"
	.position = Vec2 200, 300

	-- 设置属性
	.speed = 1.0
	.fliped = false

	-- 播放待机动画
	\play "idle", true

	-- 创建武器并装备
	sword = Sprite "assets/sword.png"
	\setSlot "right_hand", sword

	-- 注册动画结束事件
	\onAnimationEnd (animationName, target) ->
		if animationName == "attack"
			-- 攻击结束后返回待机状态
			target\play "idle", true

	-- 每隔3秒执行一次攻击
	\loop ->
		-- 播放攻击动画，不循环
		\play "attack"
		sleep 3
```

</TabItem>
</Tabs>

## 11. 结论

&emsp;&emsp;通过本教程，您已经学会了如何在 Dora SSR 引擎中使用 Playable 节点类来加载和控制各种动画模型。Playable 提供了丰富的接口，支持多种动画系统，使您能够轻松地在游戏中添加复杂的动画效果。

:::warning Spine2D 是商业软件
&emsp;&emsp;由于 Spine2D 是商业软件，使用其动画需要遵守相应的许可协议，具体请参考 [Spine 官方网站](https://esotericsoftware.com/)。
:::

&emsp;&emsp;希望本教程对您有所帮助，祝您在游戏开发中取得成功！
